﻿using System;
using System.Text.RegularExpressions;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Ooaavee.TestTools
{
    /// <summary>
    /// A better expected exception verification attribute.
    /// </summary>
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public sealed class ThrowsExceptionAttribute : ExpectedExceptionBaseAttribute
    {

        #region Private fields

        private readonly Type _expectedException;
        private string _expectedMessage;
        private string _expectedMessageAsRegex;
        private Type _expectedInnerException;
        private string _expectedInnerExceptionMessage;
        private string _expectedInnerExceptionMessageAsRegex;
        private bool _allowDerivedType;
        private bool _allowInnerExceptionDerivedType;

        private bool _checkExpectedMessage;
        private bool _checkExpectedMessageAsRegex;
        private bool _checkInnerExceptionType;
        private bool _checkInnerExceptionMessage;
        private bool _checkInnerExceptionMessageAsRegex;

        #endregion

        #region Public properties

        /// <summary>
        /// An expected exception message.
        /// </summary>
        public string ExpectedMessage
        {
            get { return _expectedMessage; }
            set
            {
                _expectedMessage = value;
                _checkExpectedMessage = true;
            }
        }

        /// <summary>
        /// An expected exception message as regular expression.
        /// </summary>
        public string ExpectedMessageAsRegex
        {
            get { return _expectedMessageAsRegex; }
            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException("value");
                }

                _expectedMessageAsRegex = value;
                _checkExpectedMessageAsRegex = true;
            }
        }

        /// <summary>
        /// An expected inner exception message.
        /// </summary>
        public string ExpectedInnerExceptionMessage
        {
            get { return _expectedInnerExceptionMessage; }
            set
            {
                _expectedInnerExceptionMessage = value;
                _checkInnerExceptionMessage = true;
            }
        }

        /// <summary>
        /// An expected inner exception message as regular expression.
        /// </summary>
        public string ExpectedInnerExceptionMessageAsRegex
        {
            get { return _expectedInnerExceptionMessageAsRegex; }
            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException("value");
                }

                _expectedInnerExceptionMessageAsRegex = value;
                _checkInnerExceptionMessageAsRegex = true;
            }
        }

        /// <summary>
        /// An expected type of inner exception.
        /// </summary>
        public Type ExpectedInnerException
        {
            get { return _expectedInnerException; }
            set
            {
                _expectedInnerException = value;
                _checkInnerExceptionType = true;
            }
        }

        /// <summary>
        /// Allow a derived type of an expected exception.
        /// </summary>
        public bool AllowDerivedType
        {
            get { return _allowDerivedType; }
            set { _allowDerivedType = value; }
        }

        /// <summary>
        /// Allow a derived type of an expected inner exception.
        /// </summary>
        public bool AllowInnerExceptionDerivedType
        {
            get { return _allowInnerExceptionDerivedType; }
            set { _allowInnerExceptionDerivedType = value; }
        }

        #endregion

        #region Constructor

        /// <summary>
        /// Initializes a new instance of the ThrowsExceptionAttribute class with an expected exception.
        /// </summary>
        /// <param name="expectedException">An expected type of exception to be thrown by a method.</param>
        public ThrowsExceptionAttribute(Type expectedException)
        {
            if (expectedException == null)
            {
                throw new ArgumentNullException("expectedException");
            }
            
            _expectedException = expectedException;
        }

        #endregion

        #region Methods

        /// <summary>
        /// Verify.
        /// </summary>
        /// <remarks>
        /// Determines whether the exception thrown is the expected exception. If the method returns, the exception was the 
        /// expected exception. If the method throws an exception, the exception was not the expected exception. The message 
        /// from the thrown exception is included in the test result. The Assert class can be used for convenience to throw 
        /// an exception. If Assert.Inconclusive is used and the assertion statement is executed, the test outcome is set to 
        /// Inconclusive.
        /// </remarks>
        /// <param name="exception">The exception that is thrown by the unit test.</param>
        protected override void Verify(Exception exception)
        {
            if (exception == null)
            {
                throw new ArgumentNullException("exception");
            }

            RethrowIfAssertException(exception);

            if (_allowDerivedType)
            {
                VerifyExceptionDerivedType(exception);
            }
            else
            {
                VerifyExceptionType(exception);
            }
            
            if (_checkExpectedMessage)
            {
                VerifyExceptionMessage(exception);
            }

            if (_checkExpectedMessageAsRegex)
            {
                VerifyExceptionMessageAsRegex(exception);
            }

            if (_checkInnerExceptionType)
            {
                if (_allowInnerExceptionDerivedType)
                {
                    VerifyInnerExceptionDerivedType(exception);
                }
                else
                {
                    VerifyInnerExceptionType(exception);
                }
            }

            if (_checkInnerExceptionMessage)
            {
                VerifyInnerExceptionMessage(exception);
            }

            if (_checkInnerExceptionMessageAsRegex)
            {
                VerifyInnerExceptionMessageAsRegex(exception);
            }            
        }

        /// <summary>
        /// Verifies an expected type of exception to be thrown by a method.
        /// </summary>
        /// <param name="exception">The exception that is thrown by the unit test.</param>
        /// <exception cref="AssertFailedException">If the result was not expected.</exception>
        private void VerifyExceptionType(Exception exception)
        {
            Type actualType = exception.GetType();

            bool passed = _expectedException.Equals(actualType);

            if (passed == false)
            {
                string msg =
                    string.Format(
                        "Test method {0}.{1} threw exception {2}, but exception {3} was expected. Exception message: {4}: {5}",
                        TestContext.FullyQualifiedTestClassName,
                        TestContext.TestName,
                        actualType.FullName,
                        _expectedException.FullName,
                        actualType.FullName,
                        exception.Message);

                AssertGateway.Fail(msg);   
            }
        }

        /// <summary>
        /// Verifies an expected type of exception to be thrown by a method.
        /// </summary>
        /// <param name="exception">The exception that is thrown by the unit test.</param>
        /// <exception cref="AssertFailedException">If the result was not expected.</exception>
        private void VerifyExceptionDerivedType(Exception exception)
        {
            Type actualType = exception.GetType();

            bool passed = _expectedException.IsAssignableFrom(actualType);

            if (passed == false)
            {
                string msg =
                    string.Format(
                        "Test method {0}.{1} threw exception {2}, but exception {3} was expected. Exception message: {4}: {5}",
                        TestContext.FullyQualifiedTestClassName,
                        TestContext.TestName,
                        actualType.FullName,
                        _expectedException.FullName,
                        actualType.FullName,
                        exception.Message);

                AssertGateway.Fail(msg);
            }
        }

        /// <summary>
        /// Verifies an expected type of inner exception to be thrown by a method.
        /// </summary>
        /// <param name="exception">The exception that is thrown by the unit test.</param>
        /// <exception cref="AssertFailedException">If the result was not expected.</exception>
        private void VerifyInnerExceptionType(Exception exception)
        {
            Exception innerException = exception.InnerException;

            if (innerException == null)
            {
                string msg =
                    string.Format(
                        "Test method {0}.{1} threw exception {2} without inner exception, but inner exception {3} was expected. Exception message: {4}: {5}.",
                        TestContext.FullyQualifiedTestClassName,
                        TestContext.TestName,
                        exception.GetType().FullName,
                        ExpectedInnerException.FullName,
                        exception.GetType().FullName,
                        exception.Message);

                AssertGateway.Fail(msg);                
            }
            else
            {
                Type actualType = innerException.GetType();

                bool passed = ExpectedInnerException.Equals(actualType);

                if (passed == false)
                {
                    string msg =
                        string.Format(
                            "Test method {0}.{1} threw exception {2} with inner exception {3}, but inner exception {4} was expected. Exception message: {5}: {6}. Inner exception message: {7}: {8}",
                            TestContext.FullyQualifiedTestClassName,
                            TestContext.TestName,
                            exception.GetType().FullName,
                            actualType.FullName,
                            ExpectedInnerException.FullName,
                            exception.GetType().FullName,
                            exception.Message,
                            actualType.FullName, 
                            innerException.Message);

                    AssertGateway.Fail(msg);
                }
            }
        }

        /// <summary>
        /// Verifies an expected type of inner exception to be thrown by a method.
        /// </summary>
        /// <param name="exception">The exception that is thrown by the unit test.</param>
        /// <exception cref="AssertFailedException">If the result was not expected.</exception>
        private void VerifyInnerExceptionDerivedType(Exception exception)
        {
            Exception innerException = exception.InnerException;

            if (innerException == null)
            {
                string msg =
                    string.Format(
                        "Test method {0}.{1} threw exception {2} without inner exception, but inner exception {3} was expected. Exception message: {4}: {5}.",
                        TestContext.FullyQualifiedTestClassName,
                        TestContext.TestName,
                        exception.GetType().FullName,
                        ExpectedInnerException.FullName,
                        exception.GetType().FullName,
                        exception.Message);

                AssertGateway.Fail(msg);
            }
            else
            {
                Type actualType = innerException.GetType();

                bool passed = ExpectedInnerException.IsAssignableFrom(actualType);

                if (passed == false)
                {
                    string msg =
                        string.Format(
                            "Test method {0}.{1} threw exception {2} with inner exception {3}, but inner exception {4} was expected. Exception message: {5}: {6}. Inner exception message: {7}: {8}",
                            TestContext.FullyQualifiedTestClassName,
                            TestContext.TestName,
                            exception.GetType().FullName,
                            actualType.FullName,
                            ExpectedInnerException.FullName,
                            exception.GetType().FullName,
                            exception.Message,
                            actualType.FullName,
                            innerException.Message);

                    AssertGateway.Fail(msg);
                }
            }
        }

        /// <summary>
        /// Verifies an expected exception message.
        /// </summary>
        /// <param name="exception">The exception that is thrown by the unit test.</param>
        /// <exception cref="AssertFailedException">If the result was not expected.</exception>
        private void VerifyExceptionMessage(Exception exception)
        {
            string message = exception.Message;

            bool passed = string.Equals(ExpectedMessage, message, StringComparison.CurrentCulture);

            if (passed == false)
            {
                string msg =
                    string.Format(
                        "Test method {0}.{1} threw exception with message {2}, but exception message {3} was expected.",
                        TestContext.FullyQualifiedTestClassName,
                        TestContext.TestName,
                        message,
                        ExpectedMessage);

                AssertGateway.Fail(msg);                
            }
        }

        /// <summary>
        /// Verifies an expected inner exception message.
        /// </summary>
        /// <param name="exception">The exception that is thrown by the unit test.</param>
        /// <exception cref="AssertFailedException">If the result was not expected.</exception>
        private void VerifyInnerExceptionMessage(Exception exception)
        {
            Exception innerException = exception.InnerException;

            if (innerException == null)
            {
                string msg =
                    string.Format(
                        "Test method {0}.{1} threw exception {2} without inner exception, but inner exception message {3} was expected. Exception message: {4}: {5}.",
                        TestContext.FullyQualifiedTestClassName,
                        TestContext.TestName,
                        exception.GetType().FullName,
                        ExpectedInnerExceptionMessage,
                        exception.GetType().FullName,
                        exception.Message);

                AssertGateway.Fail(msg);
            }
            else
            {
                string message = innerException.Message;

                bool passed = string.Equals(ExpectedInnerExceptionMessage, message, StringComparison.CurrentCulture);

                if (passed == false)
                {
                    string msg =
                        string.Format(
                            "Test method {0}.{1} threw exception with inner exception message {2}, but inner exception message {3} was expected.",
                            TestContext.FullyQualifiedTestClassName,
                            TestContext.TestName,
                            message,
                            ExpectedInnerExceptionMessage);

                    AssertGateway.Fail(msg);                    
                }
            }
        }

        /// <summary>
        /// Verifies whether the regular expression finds a match in an expection message.
        /// </summary>
        /// <param name="exception">The exception that is thrown by the unit test.</param>
        /// <exception cref="AssertFailedException">If the result was not expected.</exception>
        private void VerifyExceptionMessageAsRegex(Exception exception)
        {
            string message = exception.Message;

            bool passed = Regex.IsMatch(message, ExpectedMessageAsRegex);

            if (passed == false)
            {
                string msg =
                    string.Format(
                        "Test method {0}.{1} threw exception with message {2}, but it does not match the regular expression {3}.",
                        TestContext.FullyQualifiedTestClassName,
                        TestContext.TestName,
                        message,
                        ExpectedMessageAsRegex);

                AssertGateway.Fail(msg);
            }
        }

        /// <summary>
        /// Verifies whether the regular expression finds a match in an inner expection message.
        /// </summary>
        /// <param name="exception">The exception that is thrown by the unit test.</param>
        /// <exception cref="AssertFailedException">If the result was not expected.</exception>
        private void VerifyInnerExceptionMessageAsRegex(Exception exception)
        {
            Exception innerException = exception.InnerException;

            if (innerException == null)
            {
                string msg =
                    string.Format(
                        "Test method {0}.{1} threw exception {2} without inner exception, but it was expected to match the regular expression {3}. Exception message: {4}: {5}.",
                        TestContext.FullyQualifiedTestClassName,
                        TestContext.TestName,
                        exception.GetType().FullName,
                        ExpectedInnerExceptionMessageAsRegex,
                        exception.GetType().FullName,
                        exception.Message);

                AssertGateway.Fail(msg);
            }
            else
            {
                string message = innerException.Message;

                bool passed = Regex.IsMatch(message, ExpectedInnerExceptionMessageAsRegex);

                if (passed == false)
                {
                    string msg =
                        string.Format(
                            "Test method {0}.{1} threw exception with inner exception message {2}, but it does not match the regular expression {3}.",
                            TestContext.FullyQualifiedTestClassName,
                            TestContext.TestName,
                            message,
                            ExpectedInnerExceptionMessageAsRegex);

                    AssertGateway.Fail(msg);
                }
            }
        }

        #endregion

    }
}